/*
 *  testClassSharedRegion.cpp
 *  kangmodb
 *
 *  Created by 강모 김 on 11. 4. 25..
 *  Copyright 2011 강모소프트. All rights reserved.
 *
 */

#include "gtest/gtest.h"

#include "kangmodb.h"
#include "testCommon.h"
#include "sharedRegion.h"

/** @brief Test stgSharedRegion class 
 */
class ClassSharedRegionTest : public testing::Test {
protected:  // You should make the members protected s.t. they can be
	// accessed from sub-classes.
	
	// virtual void SetUp() will be called before each test is run.  You
	// should define it if you need to initialize the varaibles.
	// Otherwise, this can be skipped.
	virtual void SetUp() {
	}
	
	// virtual void TearDown() will be called after each test is run.
	// You should define it if there is cleanup work to do.  Otherwise,
	// you don't have to provide it.
	//
	virtual void TearDown() {
	}
protected:
};

static void * MAPPED_ADDRESS = (void*)0x200000000;
static int CHUNK_SIZE = 512;
static int MAX_CHUNK_COUNT = 3;

void BasicTest(void * mappedAddr)
{
	stgSharedRegion region;
	void * chunk1;
	void * chunk2;
	void * chunk3;
	void * chunk4;
	void * chunk;

	// Try to find out the range of address that can be mapped to a file.
	/*
	{
		for (void * addr = (void*)0x0100000000000000;
		     addr < (void*) 0xfc00000000000000;
			 addr = (void*) (((char *)addr) + 0x0100000000000000) )
		{
			kdExcp ret = region.openRegion("test.region", addr, CHUNK_SIZE, MAX_CHUNK_COUNT);
			printf("mapping at %p => %s\n", addr, (ret == 0)?"OK":"FAIL" );
			if (ret == 0)
				ASSERT_TRUE( region.closeRegion() == 0 );
		}
	}
    */
	// The basic test case
	{
		ASSERT_TRUE( region.openRegion("test.region", mappedAddr, CHUNK_SIZE, MAX_CHUNK_COUNT) == 0 );
		// Checkout. A new chunk is allocated as there is no chunk in the free chunk list.
		
		if ( mappedAddr == NULL ) // If mapped Address is NULL, stgSharedRegion uses the mapped address chosen by OS.
		{
			// Get the mapped address chosen by OS. Use it for opening the shared region next time.
			mappedAddr = region.getMappedAddress();
		}

		ASSERT_TRUE( region.checkout( & chunk1 ) == 0 );
		// Checkin. The chunk is added to the free chunk list.
		ASSERT_TRUE( region.checkin( chunk1 ) == 0 );
		// Checkout. The chunk is removed from the free chunk list.
		ASSERT_TRUE( region.checkout( & chunk1 ) == 0 );
		// Checkout. A new chunk is allocated.
		ASSERT_TRUE( region.checkout( & chunk2 ) == 0 );
		// Checkout. A new chunk is allocated.
		ASSERT_TRUE( region.checkout( & chunk3 ) == 0 );
		
		// Chunk checkout fails because MAX_CHUNK_COUNT is 3.
		ASSERT_FALSE( region.checkout( & chunk4 ) == 0 );

		// memset custom header...
		// Custom header is an area used by applications. It should not affect the behavior of stgSharedRegion.
		{
			void *customHeader;
			int customHeaderSize;
			ASSERT_TRUE( region.getCustomHeader( &customHeader, &customHeaderSize ) == 0);
			EXPECT_TRUE( customHeader != NULL );
			EXPECT_TRUE( customHeaderSize > 0 );
			memset(customHeader, 0x0403, customHeaderSize);
		}

		// Checkin. The chunk is added to the free chunk list.
		ASSERT_TRUE( region.checkin( chunk2 ) == 0 );
		// Checkin. The chunk is added to the free chunk list.
		ASSERT_TRUE( region.checkin( chunk3 ) == 0 );
		// Checkin. The chunk is added to the free chunk list.
		ASSERT_TRUE( region.checkin( chunk1 ) == 0 );

		// Checkout. The chunk is removed from the free chunk list.
		ASSERT_TRUE( region.checkout( & chunk1 ) == 0 );
		// Checkout. The chunk is removed from the free chunk list.
		ASSERT_TRUE( region.checkout( & chunk2 ) == 0 );

	
		// Initialize chunk iteration.
		ASSERT_TRUE( region.initChunkIteration() == 0 );
		// There should be two chunks.
		ASSERT_TRUE( region.iterateChunk( & chunk ) == 0 ); ASSERT_TRUE( chunk != NULL );
		ASSERT_TRUE( region.iterateChunk( & chunk ) == 0 ); ASSERT_TRUE( chunk != NULL );
		// The 3rd iteration returns NULL
		ASSERT_TRUE( region.iterateChunk( & chunk ) == 0 ); ASSERT_TRUE( chunk == NULL );
	 
		ASSERT_TRUE( region.closeRegion() == 0 );

	}
	// Success : try opening the existing region again
	{
		ASSERT_TRUE( region.openRegion("test.region", mappedAddr, CHUNK_SIZE, MAX_CHUNK_COUNT) == 0 );

		// Checkout. The chunk is removed from the free chunk list.
		ASSERT_TRUE( region.checkout( & chunk3 ) == 0 );
		// Chunk checkout fails because MAX_CHUNK_COUNT is 3. chunk1 and chunk2 was checked out before the region was opened.
		ASSERT_FALSE( region.checkout( & chunk4 ) == 0 );

		ASSERT_TRUE( region.closeRegion() == 0 );
	}

	// Success : iterate all chunks to checkin each of them.
	{
		ASSERT_TRUE( region.openRegion("test.region", mappedAddr, CHUNK_SIZE, MAX_CHUNK_COUNT) == 0 );
		// Initialize chunk iteration.
		ASSERT_TRUE( region.initChunkIteration() == 0 );
		// There should be two chunks.
		ASSERT_TRUE( region.iterateChunk( & chunk ) == 0 ); ASSERT_TRUE( chunk != NULL ); ASSERT_TRUE( region.checkin( chunk ) == 0 );
		ASSERT_TRUE( region.iterateChunk( & chunk ) == 0 ); ASSERT_TRUE( chunk != NULL ); ASSERT_TRUE( region.checkin( chunk ) == 0 );
		ASSERT_TRUE( region.iterateChunk( & chunk ) == 0 ); ASSERT_TRUE( chunk != NULL ); ASSERT_TRUE( region.checkin( chunk ) == 0 );
		// The 4th iteration returns NULL
		ASSERT_TRUE( region.iterateChunk( & chunk ) == 0 ); ASSERT_TRUE( chunk == NULL );
		ASSERT_TRUE( region.closeRegion() == 0 );
	}
	
	// Failure : try opening the existing region with different chunk size.
	ASSERT_FALSE( region.openRegion("test.region", mappedAddr, 1024, MAX_CHUNK_COUNT) == 0 );

	// Failure : try opening the existing region again with different maxium chunk count.
	ASSERT_FALSE( region.openRegion("test.region", mappedAddr, CHUNK_SIZE, MAX_CHUNK_COUNT+1) == 0 );

	// Failure : try opening the existing region with different addr.
	if (mappedAddr != NULL )
	{
		ASSERT_FALSE( region.openRegion("test.region", (void*)((char*)mappedAddr+0x10000), CHUNK_SIZE, MAX_CHUNK_COUNT) == 0 );
	}
	

	// TODO : Failure : Hits an assertion when a chunk is checked in twice.
}

TEST_F(ClassSharedRegionTest, Basic) {
	stgSharedRegion region;

	// Remove the region file if it exists.
	(void)region.removeRegionFile("test.region");


	(void)BasicTest(MAPPED_ADDRESS);

	// Success : delete the region
	{
		ASSERT_TRUE( region.removeRegionFile("test.region") == 0 );
	}
	
	// Success : open(create) the region without giving the address to map.
	(void)BasicTest(NULL);
	
	// Success : delete the region
	{
		ASSERT_TRUE( region.removeRegionFile("test.region") == 0 );
	}
}

TEST_F(ClassSharedRegionTest, PrivateMembers) {
	stgSharedRegion region;
	void * chunk1 = NULL;
	void * chunk2 = NULL;
	void * chunk3 = NULL;
	void * chunk4 = NULL;
	
	// Remove the region file if it exists.
	(void)region.removeRegionFile("test.region");


	ASSERT_TRUE( region.openRegion("test.region", MAPPED_ADDRESS, CHUNK_SIZE, MAX_CHUNK_COUNT) == 0 );
	// Checkout. A new chunk is allocated as there is no chunk in the free chunk list.

	
	// Test for allocNewChunk(). Max chunk count = 3.
	{
		// Success : allocate a new chunk
		ASSERT_TRUE( region.allocNewChunk( & chunk1 ) == 0 );
		// Success : allocate a new chunk
		ASSERT_TRUE( region.allocNewChunk( & chunk2 ) == 0 );
		// Success : allocate a new chunk
		ASSERT_TRUE( region.allocNewChunk( & chunk3 ) == 0 );
		// Failure : No more chunk.
		ASSERT_TRUE( region.allocNewChunk( & chunk4 ) == KD_EXCP_REGION_NO_MORE_CHUNK );
		ASSERT_TRUE( chunk4 == NULL );
	
	}
	
	
	// Test for hasFreeChunks, isFreeChunk
	{
		// No free chunk.
		ASSERT_FALSE( region.hasFreeChunks() );
		
		// Success : Check-in chunk2 and 3
		ASSERT_TRUE( region.checkin( chunk2 ) == 0 );

		// Has a free chunk.
		ASSERT_TRUE( region.hasFreeChunks() );

		ASSERT_TRUE( region.checkin( chunk3 ) == 0 );

		// Has some free chunks.
		ASSERT_TRUE( region.hasFreeChunks() );
		
		// chunk1 is not a free chunk yet.
		ASSERT_TRUE( ! region.isFreeChunk(chunk1) );
		// check-in chunk1.
		ASSERT_TRUE( region.checkin( chunk1 ) == 0 );
		// chunk1 is a free chunk yet.
		ASSERT_TRUE( region.isFreeChunk(chunk1) );
		// chunk1 is added to the free list. Now we have a free chunk.
		ASSERT_TRUE( region.hasFreeChunks() );
	}
	
	// Test for removeFreeChunk(), addFreeChunk()
	{
		// Remove a chunk from the free list.
		ASSERT_TRUE( region.removeFreeChunk( &chunk1 ) == 0);
		ASSERT_TRUE( region.removeFreeChunk( &chunk2 ) == 0);
		ASSERT_TRUE( region.removeFreeChunk( &chunk3 ) == 0);
		// Hits KD_ASSERT.
		//ASSERT_TRUE( region.removeFreeChunk( &chunk4 ) == 0);

		// Add a chunk to the free list.
		ASSERT_TRUE( region.addFreeChunk(chunk1) == 0);
		ASSERT_TRUE( region.addFreeChunk(chunk2) == 0);
		ASSERT_TRUE( region.addFreeChunk(chunk3) == 0);
	}
	
	// Test for makeFreeChunk()
	{
		long chunk[128];
		
		(void)stgSharedRegion::makeFreeChunk( chunk, 760403 );
		
		stgFreeChunkHeader * chunkHeader = (stgFreeChunkHeader*) chunk;
		EXPECT_TRUE( chunkHeader->freeChunkMagic == STG_FREE_CHUNK_MAGIC );
		EXPECT_TRUE( chunkHeader->nextFreeChunkId == 760403 );
	}
	
	// Test for makeAllocChunk()
	{
		long chunk[128];
		
		(void)stgSharedRegion::makeAllocChunk( chunk );
		
		stgFreeChunkHeader * chunkHeader = (stgFreeChunkHeader*) chunk;
		EXPECT_TRUE( chunkHeader->freeChunkMagic == 0 );
		EXPECT_TRUE( chunkHeader->nextFreeChunkId == 0 );
	}
	
	// Test for calcChunkAddr(), calcChunkId(), getMappedAddress()
	{
		( region.calcChunkAddr(0) == (char*)region.getMappedAddress() + stgSharedRegion::HEADER_PAGE_SIZE );
		
		EXPECT_TRUE( region.calcChunkId( region.calcChunkAddr(0) ) == 0 );
		EXPECT_TRUE( region.calcChunkId( region.calcChunkAddr(1) ) == 1 );
		EXPECT_TRUE( region.calcChunkId( region.calcChunkAddr(2) ) == 2 );
	}
	
	// Test for calcFileSize
	{
		EXPECT_TRUE( region.calcFileSize(100 /* chunk size */, 3 /* max chunk count */) == stgSharedRegion::HEADER_PAGE_SIZE + 100 * 3 + 1 );
	}
	
	ASSERT_TRUE( region.closeRegion() == 0 );

	
	// Test for openRegionFile, closeRegionFile.
	{
		bool isFileCreated = false;
		
		// open/close an existing file.
		ASSERT_TRUE( region.openRegionFile("test.region", CHUNK_SIZE, MAX_CHUNK_COUNT, &isFileCreated) == 0 );
		EXPECT_TRUE( ! isFileCreated );
		ASSERT_TRUE( region.closeRegionFile() == 0);
		
		// Remove the region file.
		ASSERT_TRUE( region.removeRegionFile("test.region") == 0 );

		// open a non-existing file
		ASSERT_TRUE( region.openRegionFile("test.region", CHUNK_SIZE, MAX_CHUNK_COUNT, &isFileCreated) == 0 );
		EXPECT_TRUE( isFileCreated );
		ASSERT_TRUE( region.closeRegionFile() == 0);
	}
}
